package org.jdns.jfreechess.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringEncoder;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.regex.Pattern;

import org.jdns.jfreechess.server.commands.Command;
import org.jdns.jfreechess.timeseal.TimesealedMessage;

import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import com.sun.istack.internal.Nullable;

/**
 * Provides bindings required to start a Netty TCP server.
 */
public class ChessServerServingModule extends AbstractModule {
  public static final SessionScope scope = new SessionScope();

  @Override
  protected final void configure() {
    bindScope(CommandScoped.class, scope);

    Multibinder<ChannelHandler> channelHandlers =
        Multibinder.newSetBinder(binder(), ChannelHandler.class);
    channelHandlers.addBinding().toProvider(new Provider<StringEncoder>() {
      @Override
      public StringEncoder get() {
        return new StringEncoder(Charsets.US_ASCII);
      }
    });

    channelHandlers.addBinding().to(TimesealNegotiationHandler.class);
    channelHandlers.addBinding().to(LoginHandler.class);
    channelHandlers.addBinding().to(SessionScopeHandler.class);
    channelHandlers.addBinding().to(TimesealedMessageHandler.class);
    channelHandlers.addBinding().to(ChannelUnregisteredHandler.class);


    bind(ChessServer.class).in(Singleton.class);

    configureChessServer();
    addUnknownCommand();
  }

  protected void configureChessServer() {}

  @Provides
  ServerBootstrap provideServerBootstrap(ChannelInitializer<SocketChannel> initializer) {
    ServerBootstrap bootstrap = new ServerBootstrap();
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 100).localAddress(5000)
        .childOption(ChannelOption.TCP_NODELAY, true).childHandler(initializer);
    return bootstrap;
  }

  private void addUnknownCommand() {
    MapBinder<Class<? extends Command>, CommandMethod> methodHandler = MapBinder.newMapBinder(
        binder(), new TypeLiteral<Class<? extends Command>>() {},
        new TypeLiteral<CommandMethod>() {});
    methodHandler.addBinding(UnknownCommand.class).toInstance(
        getHandlerMethodFor(UnknownCommand.class));
  }

  protected void addCommand(Class<? extends Command> command) {
    MapBinder<String, Command> commandHandlerMap =
        MapBinder.newMapBinder(binder(), String.class, Command.class);
    MapBinder<Class<? extends Command>, CommandMethod> methodHandler = MapBinder.newMapBinder(
        binder(), new TypeLiteral<Class<? extends Command>>() {},
        new TypeLiteral<CommandMethod>() {});

    if (!command.isAnnotationPresent(CommandName.class)) {
      throw new IllegalArgumentException(
          "Command " + command.getName() + " must be annotated with @CommandName");
    }

    String[] commandNames = command.getAnnotation(CommandName.class).names();
    if (commandNames.length == 0) {
      throw new IllegalArgumentException("Command " + command.getName()
          + " must have at least one name in its @CommandName annotation");
    }

    for (String name : commandNames) {
      commandHandlerMap.addBinding(name).to(command);
    }

    CommandMethod handlerMethod = getHandlerMethodFor(command);
    methodHandler.addBinding(command).toInstance(handlerMethod);
  }

  private CommandMethod getHandlerMethodFor(Class<? extends Command> command) {
    Method[] methods = command.getMethods();
    Method handlerMethod = null;
    for (Method method : methods) {
      if (method.isAnnotationPresent(CommandHandler.class)) {
        if (handlerMethod != null) {
          throw new IllegalStateException("Command " + command.getName()
              + " has more than one method annotated with @CommandHandler");
        }

        handlerMethod = method;
        // continue searching for CommandHandler annotations so we can crash if we find another
        // rather than silently ignoring it
      }
    }

    if (handlerMethod == null) {
      throw new IllegalStateException(
          "Command " + command.getName() + " has no method annotated with @CommandHandler");
    }
    return new CommandMethod(binder().getProvider(Injector.class), command, handlerMethod);
  }

  @Provides
  ChannelInitializer<SocketChannel> provideSocketChannelInitializer(
      PlayerChannelInitializer initializer) {
    return initializer;
  }

  @Provides
  @CommandScoped
  Channel provideSessionChannel() {
    // this value is provided by the SessionScopeHandler, we should never get here
    throw new IllegalStateException(
        "I can't provide Channel objects, " + "only the SesssionScopeHandler can");
  }

  @Provides
  @CommandScoped
  TimesealedMessage provideTimesealedMessage() {
    // this value is provided by the SessionScopeHandler, we should never get here
    throw new IllegalStateException(
        "I can't provide TimesealedMessage objects, " + "only the SesssionScopeHandler can");
  }

  @Provides
  @UserCommand
  String provideUserCommandName(TimesealedMessage message) {
    return Iterables.getFirst(Splitter.on(Pattern.compile("\\s+")).split(message.getMessage()),
        null);
  }

  @Provides
  @CommandScoped
  @UserCommand
  Command provideUserCommand(TimesealedMessage message,
      @Nullable @UserCommand String userCommandName, Map<String, Provider<Command>> commandNames,
      UnknownCommand unknownCommand) {
    if (userCommandName.startsWith("$")) {
      userCommandName = userCommandName.substring(1);
    }

    if (userCommandName == null || !commandNames.containsKey(userCommandName)) {
      return unknownCommand;
    }

    return commandNames.get(userCommandName).get();
  }

  @Provides
  @CommandScoped
  @UserCommand
  CommandMethod provideCommandMethod(@UserCommand final Command command,
      Map<Class<? extends Command>, CommandMethod> methodMap) {
    return methodMap.get(command.getClass());
  }

  @Provides
  @CommandScoped
  User getLoggedInUser(Channel channel) {
    return channel.attr(User.CHANNEL_ATTRIBUTE_KEY).get();
  }

  @Provides
  @ConnectedPlayers
  Map<String, User> provideConnectedPlayers(ChessServer server) {
    return server.getConnectedUsersByName();
  }
}
